﻿#region

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Reflection;
using System.Text;
using System.Xml;
using BTL.Technology.Framework;

#endregion

namespace BTL.Technology.Data
{
    internal class ObjectHelper
    {
        public static TObject FillObject<TObject>(IDataReader dr, bool closeReader = true)
        {
            return (TObject) CreateObjectFromReader(typeof (TObject), dr, closeReader);
        }

        /// -----------------------------------------------------------------------------
        /// <summary>
        ///   FillCollection fills a Collection of objects from a DataReader
        /// </summary>
        /// <typeparam name="TItem"> The type of object </typeparam>
        /// <param name="dr"> The Data Reader </param>
        /// <param name="closeReader"> </param>
        /// <history></history>
        /// -----------------------------------------------------------------------------
        public static List<TItem> FillCollection<TItem>(IDataReader dr, bool closeReader = true)
        {
            return (List<TItem>) FillListFromReader(dr, new List<TItem>(), closeReader);
        }

        /// -----------------------------------------------------------------------------
        /// <summary>
        ///   SerializeObject serializes an Object
        /// </summary>
        /// <param name="objObject"> The object to Initialise </param>
        /// -----------------------------------------------------------------------------
        public static string SerializeObject(object objObject)
        {
            return SerializeObject(objObject, null);
        }

        /// -----------------------------------------------------------------------------
        /// <summary>
        ///   SerializeObject serializes an Object
        /// </summary>
        /// <param name="objObject"> The object to Initialise </param>
        /// <param name="root"> name of the root element </param>
        /// -----------------------------------------------------------------------------
        public static string SerializeObject(object objObject, string root)
        {
            if (objObject is ICollection)
            {
                return SerializeCollection(objObject, root);
            }

            var stringBuilder = new StringBuilder();
            using (var writer = XmlWriter.Create(stringBuilder))
            {
                var serializer = new DataXmlSerializer(objObject.GetType());
                serializer.Serialize(writer, objObject, root);

                writer.Flush();
            }
            return stringBuilder.ToString();
        }

        /// -----------------------------------------------------------------------------
        /// <summary>
        ///   SerializeCollection serializes an ICollection
        /// </summary>
        /// <param name="objCollection"> The object to Initialise </param>
        /// -----------------------------------------------------------------------------
        public static string SerializeCollection(object objCollection)
        {
            return SerializeCollection(objCollection, null);
        }

        /// -----------------------------------------------------------------------------
        /// <summary>
        ///   SerializeCollection serializes an ICollection
        /// </summary>
        /// <param name="objCollection"> The object to Initialise </param>
        /// <param name="root"> name of the root element </param>
        /// -----------------------------------------------------------------------------
        public static string SerializeCollection(object objCollection, string root)
        {
            return SerializeCollection(objCollection, root, Encoding.Default);
        }

        /// -----------------------------------------------------------------------------
        /// <summary>
        ///   SerializeCollection serializes an ICollection
        /// </summary>
        /// <param name="objCollection"> The object to Initialise </param>
        /// <param name="root"> name of the root element </param>
        /// <param name="encoding"> </param>
        /// -----------------------------------------------------------------------------
        public static string SerializeCollection(object objCollection, string root, Encoding encoding)
        {
            if (objCollection == null)
                return string.Format("<{0} />", string.IsNullOrEmpty(root) ? "Root" : root);

            var stringBuilder = new StringBuilder();

            //XmlWriterSettings settings = new XmlWriterSettings();
            //if (addEncoding != Encoding.Default) settings.Encoding = Encoding.UTF8;  
            //settings.Indent = true;

            using (var writer = XmlWriter.Create(stringBuilder))
            {
                if (encoding != Encoding.Default)
                    writer.WriteProcessingInstruction("xml",
                                                      string.Format("version=\"1.0\" encoding=\"{0}\"",
                                                                    encoding.BodyName));

                var serializer = new DataXmlSerializer(objCollection.GetType());
                serializer.Serialize(writer, objCollection as ICollection, root);

                writer.Flush();
            }
            return stringBuilder.ToString();
        }

        /// -----------------------------------------------------------------------------
        /// <summary>
        ///   FillListFromReader fills a list of objects of a specified type from a DataReader
        /// </summary>
        /// <typeparam name="TItem"> The type of the business object </typeparam>
        /// <param name="dr"> The IDataReader to use to fill the objects </param>
        /// <param name="objList"> The List to Fill </param>
        /// <param name="closeReader"> A flag that indicates whether the DataReader should be closed </param>
        /// <returns> A List of objects (TItem) </returns>
        /// <remarks>
        /// </remarks>
        /// <history></history>
        /// -----------------------------------------------------------------------------
        private static IList<TItem> FillListFromReader<TItem>(IDataReader dr, IList<TItem> objList, bool closeReader)
        {
            var isSuccess = false;

            try
            {
                // iterate datareader
                while (dr.Read())
                {
                    //Create the Object
                    var objObject = (TItem) CreateObjectFromReader(typeof (TItem), dr, false);

                    // add to collection
                    objList.Add(objObject);
                }
                isSuccess = true;
            }
            finally
            {
                // Ensure DataReader is closed
                if ((!isSuccess))
                    closeReader = true;
                CloseDataReader(dr, closeReader);
            }

            return objList;
        }


        private static object CreateObjectFromReader(Type objType, IDataReader dr, bool closeReader,
                                                     bool isOpened = false)
        {
            object objObject = null;
            var isSuccess = false;
            var canRead = true;

            if (closeReader && !isOpened)
            {
                canRead = false;
                // read datareader
                if (dr.Read())
                {
                    canRead = true;
                }
            }

            try
            {
                if (canRead)
                {
                    //Create the Object
                    objObject = CreateObject(objType, false);

                    //hydrate the custom business object
                    FillObjectFromReader(objObject, dr);
                }
                isSuccess = true;
            }
            finally
            {
                // Ensure DataReader is closed
                if ((!isSuccess))
                    closeReader = true;
                CloseDataReader(dr, closeReader);
            }

            return objObject;
        }


        private static void CloseDataReader(IDataReader dr, bool closeReader)
        {
            // close datareader
            if ((dr != null) && closeReader)
            {
                dr.Close();
            }
        }


        /// -----------------------------------------------------------------------------
        /// <summary>
        ///   CreateObject creates a new object.
        /// </summary>
        /// <param name="objType"> The type of object to create. </param>
        /// <param name="initialise"> A flag that indicates whether to initialise the object. </param>
        /// <history></history>
        /// -----------------------------------------------------------------------------
        private static object CreateObject(Type objType, bool initialise)
        {
            var objObject = Activator.CreateInstance(objType);

            if (initialise)
            {
                //InitializeObject(objObject);
            }

            return objObject;
        }

        /// -----------------------------------------------------------------------------
        /// <summary>
        ///   FillObjectFromReader fills an object from the provided DataReader. If the object implements the IHydratable interface it will use the object's IHydratable.Fill() method. Otherwise, it will use reflection to fill the object.
        /// </summary>
        /// <param name="objObject"> The object to fill </param>
        /// <param name="dr"> The DataReader </param>
        /// <history></history>
        /// -----------------------------------------------------------------------------
        private static void FillObjectFromReader(object objObject, IDataReader dr)
        {
            try
            {
                //Determine if object is IHydratable
                if (objObject is IEntityobject)
                {
                    //Use IHydratable's Fill
                    var objHydratable = objObject as IEntityobject;
                    objHydratable.Fill(dr);
                }
                else
                {
                    //Use Reflection
                    FillNonEntityObject(objObject, dr);
                }
            }
            catch (IndexOutOfRangeException iex)
            {
                //TODO: CustomError - Implement error
                throw new Exception("", iex);
            }
        }

        /// -----------------------------------------------------------------------------
        /// <summary>
        ///   HydrateObject uses reflection to hydrate an object.
        /// </summary>
        /// <param name="hydratedObject"> The object to Hydrate </param>
        /// <param name="dr"> The IDataReader that contains the columns of data for the object </param>
        /// <history></history>
        /// -----------------------------------------------------------------------------
        private static void FillNonEntityObject(object hydratedObject, IDataReader dr)
        {
            int intIndex;

            // get cached object mapping for type
            var objMappingInfo = ObjectMappingInfo.GetObjectMapping(hydratedObject.GetType());


            // fill object with values from datareader
            for (intIndex = 0; intIndex <= dr.FieldCount - 1; intIndex++)
            {
                //If the Column matches a Property in the Object Map's PropertyInfo Dictionary
                var fieldname = dr.GetName(intIndex).ToUpperInvariant();
                PropertyInfo objPropertyInfo;
                if (objMappingInfo.Properties.TryGetValue(fieldname, out objPropertyInfo))
                {
                    SetNonEntityObject(hydratedObject, objPropertyInfo, dr.GetValue(intIndex));
                }
                else if (fieldname.IndexOf(".", StringComparison.Ordinal) > 0)
                {
                    var list = fieldname.Split('.');
                    if (list.Length >= 2)
                    {
                        var chieldOjectName = list[0];
                        var chieldFieldName = list[1];

                        if (objMappingInfo.Properties.TryGetValue(chieldOjectName, out objPropertyInfo))
                        {
                            var exitingValue = objPropertyInfo.GetValue(hydratedObject, null) ??
                                               CreateObject(objPropertyInfo.PropertyType, false);

                            var value = dr.GetValue(intIndex);

                            // get cached object mapping for type
                            var objChieldMappingInfo =
                                ObjectMappingInfo.GetObjectMapping(exitingValue.GetType());

                            PropertyInfo childPropertyInfo = null;

                            if (objChieldMappingInfo.Properties.TryGetValue(chieldFieldName, out childPropertyInfo))
                            {
                                SetNonEntityObject(exitingValue, childPropertyInfo, value);

                                objPropertyInfo.SetValue(hydratedObject, exitingValue, null);
                            }
                        }
                    }
                }
            }
        }

        private static void SetNonEntityObject(object hydratedObject, PropertyInfo objPropertyInfo, object value)
        {
            Type propType = null;

            //Get its type
            propType = objPropertyInfo.PropertyType;

            //If property can be set
            if (!objPropertyInfo.CanWrite) return;

            //Get the Data Value from the data reader
            var coloumnValue = value;

            //Get the Data Value's type
            var objDataType = coloumnValue.GetType();

            if (coloumnValue == DBNull.Value)
            {
                // set property value to Null
                //TODO: default(objPropertyInfo.PropertyType)
                //objPropertyInfo.SetValue(hydratedObject, default(objPropertyInfo.PropertyType), null);
            }
            else if (propType == objDataType)
            {
                //Property and data objects are the same type
                objPropertyInfo.SetValue(hydratedObject, coloumnValue, null);
            }
            else
            {
                // business object info class member data type does not match datareader member data type

                //need to handle enumeration conversions differently than other base types
                if (propType.BaseType != null && propType.BaseType == typeof (Enum))
                {
                    // check if value is numeric and if not convert to integer ( supports databases like Oracle )
                    if (Char.IsNumber(coloumnValue.ToString(), 0))
                    {
                        objPropertyInfo.SetValue(hydratedObject,
                                                 Enum.ToObject(propType, Convert.ToInt32(coloumnValue)), null);
                    }
                    else
                    {
                        objPropertyInfo.SetValue(hydratedObject, Enum.ToObject(propType, coloumnValue), null);
                    }
                }
                else if (ReferenceEquals(propType, typeof (Guid)))
                {
                    // guid is not a datatype common across all databases ( ie. Oracle )
                    objPropertyInfo.SetValue(hydratedObject,
                                             Convert.ChangeType(new Guid(coloumnValue.ToString()), propType), null);
                }
                else if (ReferenceEquals(propType, typeof (Version)))
                {
                    objPropertyInfo.SetValue(hydratedObject, new Version(coloumnValue.ToString()), null);
                }
                else if ((coloumnValue is IConvertible))
                {
                    objPropertyInfo.SetValue(hydratedObject, Convert.ChangeType(coloumnValue, propType), null);
                }
                else
                {
                    // try explicit conversion
                    objPropertyInfo.SetValue(hydratedObject, coloumnValue, null);
                }
            }
        }
    }

    public interface IEntityobject
    {
        int KeyId { get; set; }
        void Fill(IDataReader dr);
    }
}